package interfaces.clients.authorization;

import interfaces.clients.authorization.exceptions.*;

import interfaces.common.ServerSettings;

import semaphores.ISmartSemaphore;
import semaphores.SmartSemaphore;

import java.io.*;
import java.util.Iterator;

/**
 * The class implements authorization.
 *
 * @author Manujlov Maxim
 */
public final class AuthorizationSystem implements Authorizer {
  private static AuthorizationSystem instance = null;


  /**
   * The function returns the instance of authorization system.
   *
   * @return the instance of authorization system
   */
  public static Authorizer getInstance() {
    if (instance == null) instance = new AuthorizationSystem();
    return instance;
  }

  /**
   * Constructor
   */
  private AuthorizationSystem() {
    sem.exclusiveDown();
    try {
      String path = ServerSettings.getWorkingDir();
      String fileName = ServerSettings.getUsersDatabaseFileName();
      File database = new File(path, fileName);
      FileInputStream fis;
      try {
        fis = new FileInputStream(database);
        DataInputStream dis = new DataInputStream(fis);
        try {
          while (dis.available() != 0) {
            int id = dis.readInt();
            String name = dis.readUTF();
            String pass = dis.readUTF();
            users.put(name, new UserData(id, name, pass));
          }
          dis.close();
          fis.close();
        }
        catch (IOException e) {
          throw new UnknownErrorException("The database cannot be opened due to \"" + e.getMessage() + "\".");
        }
      }
      catch (FileNotFoundException e) {
        (new DataSaver()).start();
      }
    }
    finally {
      sem.exclusiveUp();
    }
  }

  /**
   * The inner function of authorization. This function specially doesn't use synchronization
   * because it[function] is executed from the other functions.
   *
   * @param data user authorization data
   * @throws UserDoesNotExistException if the user does not exist
   * @throws WrongPasswordException if the password is wrong
   * @return user data
   */
  private UserData inner_authorize(AuthorizationData data) throws UserDoesNotExistException, WrongPasswordException {
    if (data.getUsername().compareTo("") == 0) return new UserData(0, "", "");
    UserData user = (UserData)users.get(data.getUsername());
    if (user == null) {
      throw new UserDoesNotExistException("There is no user with the name \"" + data.getUsername() + "\".");
    }
    if (user.getPassword().compareTo(data.getPassword()) != 0) throw new WrongPasswordException("Password is wrong.");
    return user;
  }

  /**
   * The function gets user data and returns user id.
   *
   * @param data user authorization data
   * @throws UserDoesNotExistException if the user does not exist
   * @throws WrongPasswordException if the password is wrong
   * @return user data
   */
  public final UserData authorize(AuthorizationData data) throws UserDoesNotExistException, WrongPasswordException {
    sem.down();
    try {
      return inner_authorize(data);
    }
    finally {
      sem.up();
    }
  }

  /**
   * The function returns user data.
   *
   * @param uName user name
   * @throws UserDoesNotExistException if the user doesn't exist
   * @return user data
   */
  public UserData getUserData(String uName) throws UserDoesNotExistException {
    sem.down();
    try {
      if (uName.compareTo("") == 0) return new UserData(0, "", "");
      UserData user = (UserData)users.get(uName);
      if (user == null) throw new UserDoesNotExistException("There is no user with the name \"" + uName + "\".");
      return user;
    }
    finally {
      sem.up();
    }
  }

  /**
   * The function adds user to database.
   *
   * @param data user authorization data
   * @throws MaximallyPossibleQuantityOfTheUsersException if the quantity of the users is maximally possible
   * @throws ImpossibleStringException if the name or the password is longer than 65535 symbols
   * @throws UserAlreadyExistsException if the user already exists
   * @return user data
   */
  public final UserData addUser(AuthorizationData data) throws UserAlreadyExistsException, ImpossibleStringException,
                                                               MaximallyPossibleQuantityOfTheUsersException {
    sem.exclusiveDown();
    try {
      if (getUsersCount() == Integer.MAX_VALUE) {
        throw new MaximallyPossibleQuantityOfTheUsersException("The quantity of the users is maximally possible.");
      }
      String name = data.getUsername();
      if (users.containsKey(name)) {
        throw new UserAlreadyExistsException("The user with the name \"" + name + "\" already exists.");
      }
      if (name.compareTo("") == 0) throw new ImpossibleStringException("The name is empty.");
      if (name.length() > 65535) {
        throw new ImpossibleStringException("The name is too long. The limit is 65535 symbols.");
      }
      if (data.getPassword().length() > 65535) {
        throw new ImpossibleStringException("The password is too long. The limit is 65535 symbols.");
      }
      users.put(name, new UserData(users.makeNewId(), data));
      (new DataSaver()).start();
      return (UserData)users.get(name);
    }
    finally {
      sem.exclusiveUp();
    }
  }

  /**
   * The function deletes user from database.
   *
   * @param data user authorization data
   * @throws UserDoesNotExistException if the user does not exist
   * @throws WrongPasswordException if the password is wrong
   * @return user data
   */
  public final UserData deleteUser(AuthorizationData data) throws UserDoesNotExistException, WrongPasswordException {
    sem.exclusiveDown();
    try {
      inner_authorize(data);
      String name = data.getUsername();
      if (name.compareTo("") == 0) return new UserData(0, "", "");
      UserData user = (UserData)users.get(name);
      users.remove(name);
      (new DataSaver()).start();
      return user;
    }
    finally {
      sem.exclusiveUp();
    }
  }

  /**
   * The function changes user name.
   *
   * @param data user authorization data
   * @param newName new user name
   * @throws UserDoesNotExistException if the user does not exist
   * @throws WrongPasswordException if the password is wrong
   * @throws UserAlreadyExistsException if the user already exists
   * @return user data
   */
  public final UserData changeUserName(AuthorizationData data, String newName) throws UserDoesNotExistException,
                                                                                      WrongPasswordException,
                                                                                      UserAlreadyExistsException {
    sem.exclusiveDown();
    try {
      inner_authorize(data);
      String name = data.getUsername();
      if (name.compareTo("") == 0) return new UserData(0, "", "");
      if (users.containsKey(newName)) {
        throw new UserAlreadyExistsException("The user with the name \"" + newName + "\" already exists.");
      }
      UserData oldUser = (UserData)users.get(name);
      users.remove(name);
      UserData newUser = (UserData)users.put(newName, new UserData(oldUser.getId(), newName, oldUser.getPassword()));
      (new DataSaver()).start();
      return newUser;
    }
    finally {
      sem.exclusiveUp();
    }
  }

  /**
   * The function changes user password.
   *
   * @param data user authorization data
   * @param newPwd new user password
   * @throws UserDoesNotExistException if the user does not exist
   * @throws WrongPasswordException if the password is wrong
   * @return user data
   */
  public final UserData changeUserPassword(AuthorizationData data, String newPwd) throws UserDoesNotExistException,
                                                                                         WrongPasswordException {
    sem.exclusiveDown();
    try {
      inner_authorize(data);
      String name = data.getUsername();
      if (name.compareTo("") == 0) return new UserData(0, "", "");
      UserData oldUser = (UserData)users.get(name);
      users.remove(name);
      UserData newUser = (UserData)users.put(name, new UserData(oldUser.getId(), name, newPwd));
      (new DataSaver()).start();
      return newUser;
    }
    finally {
      sem.exclusiveUp();
    }
  }

  /**
   * The function checks if the user is in the database.
   *
   * @param uName user name
   * @return true, if the user exists, false, otherwise
   */
  public final boolean userExists(String uName) {
    return users.containsKey(uName);
  }

  /**
   * The function returns the quantity of the users.
   *
   * @return the quantity of the users
   */
  public final int getUsersCount() {
    return users.size();
  }

  /**
   * The semaphore for synchronization
   */
  private final ISmartSemaphore sem = new SmartSemaphore();

  /**
   * The users database.
   */
  private final IUsersDataBase users = new UsersDataBase();

  /**
   * The thread saves changes of database in the file.
   */
  private final class DataSaver extends Thread {
    public void run () {
      sem.down();
      try {
        String path = ServerSettings.getWorkingDir();
        String fileName = ServerSettings.getUsersDatabaseFileName();
        File database = new File(path, fileName);
        FileOutputStream fos = null;
        try {
          fos = new FileOutputStream(database);
        }
        catch (FileNotFoundException e) {
        }
        DataOutputStream dos = new DataOutputStream(fos);
        Iterator it = users.keySet().iterator();
        String name;
        while (it.hasNext()) {
          name = (String)it.next();
          UserData data = (UserData)users.get(name);
          try {
            dos.writeInt(data.getId());
            dos.writeUTF(name);
            dos.writeUTF(data.getPassword());
          }
          catch (IOException e) {
            throw new UnknownErrorException("The database cannot be saved due to \"" + e.getMessage() + "\".");
          }
        }
        try {
          dos.close();
          if (fos != null) fos.close();
        }
        catch (IOException e) {
          throw new UnknownErrorException("The database cannot be saved due to \"" + e.getMessage() + "\".");
        }
      }
      finally {
        sem.up();
      }
    }
  }
}
